重要提醒:pl.Categorical
在v.1.32.0
進行了重大變更,本日內容將會以新版使用方式說明(v.1.33.1
)。
今天我們來了解pl.Enum與pl.Categorical兩種型別的使用時機。
這兩種型別都是針對有限種類的pl.String
型別而設計,因此舉凡四季或月份等可以列舉的事物,都很適合使用。依靠這兩個型別,Polars將不用真的儲存每一行,而是可以依靠編碼及索引關係來取值,這將大幅減少儲存空間及提升存取效率。
pl.Enum
適用在可以事先確定所有列舉可能的時候,使用起來比較簡單;而pl.Categorical
更適合用在事先無法確定所有列舉可能的時候。
本日內容將會以三種常用的作業系統做為例子,其順序為隨機定義,並無特別含義。
本日大綱如下:
pl.Categorical
重要變更pl.Enum
pl.Categorical
codepanda
import polars as pl
os_data = ["macOS", "Linux", "Windows"]
pl.Categorical
重要變更pl.Categorical
在v.1.32.0
進行了重大變更,主要有兩點影響。
ordering=
參數ordering=
將永遠為「"lexical"」,並廢除「"physical"」。
根據Ritchie Vink(Polars創始人)在LinkedIn的貼文,或許以後使用者將不需在意惱人的String cache,但教學文件尚未更新。
以下是目前教學中文件的範例:
from polars.exceptions import StringCacheMismatchError
bears_cat = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
try:
print(bears_cat == bears_cat2)
except StringCacheMismatchError as exc:
exc_str = str(exc).splitlines()[0]
print("StringCacheMismatchError:", exc_str)
StringCacheMismatchError: cannot compare categoricals coming from different sources, consider setting a global StringCache.
這個錯誤訊息是指當pl.Categorical
型別是分開被定義(即分別進行編碼)時,Polars將無法進行有效的運算。解決的方法有以下兩種:
使用pl.enable_string_cache():只要在使用pl.Categorical
前,加上pl.enable_string_cache()
,就可以在全域範圍內使用string cache。官方文件特別提醒這是一個非常沒有效率的解決方法(雖然Rust很快...),只推薦在必要時刻使用。
使用pl.StringCache():pl.StringCache()
可以做為context manager或是decorator使用,這將使得Polars可以在局部範圍內,針對分開定義的pl.Categorical
型別,進行有效率的編碼。
pl.StringCache()
做為context manager使用:with pl.StringCache():
bears_cat = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
print(bears_cat == bears_cat2)
pl.StringCache()
做為decorator使用:@pl.StringCache()
def compare_bears() -> pl.Series:
bears_cat = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
print(bears_cat == bears_cat2)
compare_bears()
pl.Enum
建立pl.Enum
最簡單的方式為傳入一個iterable,如一個列表:
enum_order = ["Linux", "macOS", "Windows"]
# "Linux" < "macOS" < "Windows"
common_os_enum = pl.Enum(enum_order)
此時common_os_enum
即為pl.Enum
型別,且具備可比較性("Linux" < "macOS" < "Windows"
)。
以下我們建立一個common_os_enum_df
dataframe,內含「"os"」、「"os2"」及「"os3"」三列。其中「"os"」及「"os2"」列為pl.Enum
型別,「"os3"」列為pl.String
型別:
common_os_enum_df = (
pl.DataFrame(
{"os": os_data},
schema={"os": common_os_enum},
)
.with_columns(pl.col("os").shuffle(seed=42).alias("os2"))
.with_columns(pl.col("os2").cast(pl.String).alias("os3"))
)
shape: (3, 3)
┌─────────┬─────────┬─────────┐
│ os ┆ os2 ┆ os3 │
│ --- ┆ --- ┆ --- │
│ enum ┆ enum ┆ str │
╞═════════╪═════════╪═════════╡
│ macOS ┆ Windows ┆ Windows │
│ Linux ┆ Linux ┆ Linux │
│ Windows ┆ macOS ┆ macOS │
└─────────┴─────────┴─────────┘
這邊需留意,如果「"os"」及 「"os2"」列中有不屬於enum_order
元素的話,Polars將會回報InvalidOperationError
。
pl.Enum
可以與字串比較,但該字串必須是構成pl.Enum
的元素之一,否則會回報InvalidOperationError
。例如我們使用pl.DataFrame.filter()
來篩選出「"os"」列中大於「"macOS"」字串的行數:
# "Linux" < "macOS" < "Windows"
common_os_enum_df.filter(pl.col("os").gt("macOS"))
shape: (1, 3)
┌─────────┬───────┬───────┐
│ os ┆ os2 ┆ os3 │
│ --- ┆ --- ┆ --- │
│ enum ┆ enum ┆ str │
╞═════════╪═══════╪═══════╡
│ Windows ┆ macOS ┆ macOS │
└─────────┴───────┴───────┘
因為common_os_enum
的排序大小為"Linux" < "macOS" < "Windows"
,所以:
「"macOS"」 > 「"macOS"」?
=> False
「"Linux"」 > 「"macOS"」?
=> False
「"Windows"」 > 「"macOS"」?
=> True
只有最後一行符合篩選條件。
pl.String
型別比較pl.Enum
可以與pl.String
型別比較,但pl.String
之字串必須是構成pl.Enum
的元素之一,否則會回報InvalidOperationError
。例如我們可以計算「"os"」列(pl.Enum
型別)是否大於「"os3"」(pl.String
型別)列:
# "Linux" < "macOS" < "Windows"
(
common_os_enum_df.with_columns(
pl.col("os").gt(pl.col("os3")).alias("os > os3")
)
)
shape: (3, 4)
┌─────────┬─────────┬─────────┬──────────┐
│ os ┆ os2 ┆ os3 ┆ os > os3 │
│ --- ┆ --- ┆ --- ┆ --- │
│ enum ┆ enum ┆ str ┆ bool │
╞═════════╪═════════╪═════════╪══════════╡
│ macOS ┆ Windows ┆ Windows ┆ false │
│ Linux ┆ Linux ┆ Linux ┆ false │
│ Windows ┆ macOS ┆ macOS ┆ true │
└─────────┴─────────┴─────────┴──────────┘
因為common_os_enum
的排序大小為"Linux" < "macOS" < "Windows"
,所以:
「"macOS"」 > 「"Windows"」?
=> False
「"Linux"」 > 「"Linux"」?
=> False
「"Windows"」 > 「"macOS"」?
=> True
pl.Enum
型別比較pl.Enum
可以與pl.Enum
型別比較,但必須是由相同的元素建構而成,否則會回報InvalidOperationError
。例如我們可以計算「"os"」列(pl.Enum
型別)是否大於「"os2"」(pl.Enum
型別)列:
# "Linux" < "macOS" < "Windows"
(
common_os_enum_df.with_columns(
pl.col("os").gt(pl.col("os2")).alias("os > os2")
)
)
shape: (3, 4)
┌─────────┬─────────┬─────────┬──────────┐
│ os ┆ os2 ┆ os3 ┆ os > os2 │
│ --- ┆ --- ┆ --- ┆ --- │
│ enum ┆ enum ┆ str ┆ bool │
╞═════════╪═════════╪═════════╪══════════╡
│ macOS ┆ Windows ┆ Windows ┆ false │
│ Linux ┆ Linux ┆ Linux ┆ false │
│ Windows ┆ macOS ┆ macOS ┆ true │
└─────────┴─────────┴─────────┴──────────┘
因為common_os_enum
的排序大小為"Linux" < "macOS" < "Windows"
,所以:
「"macOS"」 > 「"Windows"」?
=> False
「"Linux"」 > 「"Linux"」?
=> False
「"Windows"」 > 「"macOS"」?
=> True
pl.Categorical
以下我們建立一個common_os_cat_df
dataframe,內含「"os"」、「"os2"」及「"os3"」三列。其中「"os"」及「"os2"」列為pl.Categorical
型別,「"os3"」列為pl.String
型別:
common_os_cat_df = (
pl.DataFrame({"os": os_data}, schema={"os": pl.Categorical()})
.with_columns(pl.col("os").shuffle(seed=42).alias("os2"))
.with_columns(pl.col("os2").cast(pl.String).alias("os3"))
)
shape: (3, 3)
┌─────────┬─────────┬─────────┐
│ os ┆ os2 ┆ os3 │
│ --- ┆ --- ┆ --- │
│ cat ┆ cat ┆ str │
╞═════════╪═════════╪═════════╡
│ macOS ┆ Windows ┆ Windows │
│ Linux ┆ Linux ┆ Linux │
│ Windows ┆ macOS ┆ macOS │
└─────────┴─────────┴─────────┘
pl.Categorical
可以與字串進行比較,例如我們使用pl.DataFrame.filter()
來篩選出「"os"」列中大於「"Windows"」字串的行數:
# ord("L")=76, ord("W")=87, ord("m")=109,
# "Linux" < "Windows" < "macOS"
common_os_cat_df.filter(pl.col("os").gt("Windows"))
shape: (1, 3)
┌───────┬─────────┬─────────┐
│ os ┆ os2 ┆ os3 │
│ --- ┆ --- ┆ --- │
│ cat ┆ cat ┆ str │
╞═══════╪═════════╪═════════╡
│ macOS ┆ Windows ┆ Windows │
└───────┴─────────┴─────────┘
由於各行開頭字母皆不一樣,所以我們可以只計算開頭字母的ord()
結果。因為排序為"Linux" < "Windows" < "macOS"
,所以:
「"macOS"」 > 「"Windows"」?
=> True
「"Linux"」 > 「"Windows"」?
=> False
「"Windows"」 > 「"Windows"」?
=> False
只有第一行符合篩選條件。
pl.String
型別比較pl.Categorical
可以與pl.String
型別比較。例如我們可以計算「"os"」列(pl.Categorical
型別)是否大於「"os3"」(pl.String
型別)列:
# ord("L")=76, ord("W")=87, ord("m")=109,
# "Linux" < "Windows" < "macOS"
(
common_os_cat_df.with_columns(
pl.col("os").gt(pl.col("os3")).alias("os > os3"),
)
)
shape: (3, 4)
┌─────────┬─────────┬─────────┬──────────┐
│ os ┆ os2 ┆ os3 ┆ os > os3 │
│ --- ┆ --- ┆ --- ┆ --- │
│ cat ┆ cat ┆ str ┆ bool │
╞═════════╪═════════╪═════════╪══════════╡
│ macOS ┆ Windows ┆ Windows ┆ true │
│ Linux ┆ Linux ┆ Linux ┆ false │
│ Windows ┆ macOS ┆ macOS ┆ false │
└─────────┴─────────┴─────────┴──────────┘
因為排序為"Linux" < "Windows" < "macOS"
,所以:
「"macOS"」 > 「"Windows"」?
=> True
「"Linux"」 > 「"Linux"」?
=> False
「"Windows"」 > 「"macOS"」?
=> False
pl.Categorical
型別比較pl.Categorical
可以與pl.Categorical
型別比較。例如我們可以計算「"os"」列(pl.Categorical
型別)是否大於「"os2"」(pl.Categorical
型別)列:
# ord("L")=76, ord("W")=87, ord("m")=109,
# "Linux" < "Windows" < "macOS"
(
common_os_cat_df.with_columns(
pl.col("os").gt(pl.col("os2")).alias("os > os2"),
)
)
shape: (3, 4)
┌─────────┬─────────┬─────────┬──────────┐
│ os ┆ os2 ┆ os3 ┆ os > os2 │
│ --- ┆ --- ┆ --- ┆ --- │
│ cat ┆ cat ┆ str ┆ bool │
╞═════════╪═════════╪═════════╪══════════╡
│ macOS ┆ Windows ┆ Windows ┆ true │
│ Linux ┆ Linux ┆ Linux ┆ false │
│ Windows ┆ macOS ┆ macOS ┆ false │
└─────────┴─────────┴─────────┴──────────┘
因為排序為"Linux" < "Windows" < "macOS"
,所以:
「"macOS"」 > 「"Windows"」?
=> True
「"Linux"」 > 「"Linux"」?
=> False
「"Windows"」 > 「"macOS"」?
=> False
pl.Expr.cat
命名空間pl.Expr.cat命名空間有提供少數expr。
這裡我們展示如何利用pl.Expr.cat.get_categories()得到pl.Categorical
內的元素:
common_os_cat_df.select(pl.col("os").cat.get_categories())
shape: (3, 1)
┌─────────┐
│ os │
│ --- │
│ str │
╞═════════╡
│ macOS │
│ Linux │
│ Windows │
└─────────┘
其結果與選取「"os"」列一樣,但那是因為「"os"」列內剛好只有這三個元素。
此外,不知道眼尖的您有沒有發現,返回的的「"os"」列是pl.String
型別。
codepanda
Pandas中相對應於polars的pl.Enum
及pl.Categorical
中的功能是pd.CategoricalDtype。
pd.CategoricalDtype
可以分為無序及有序兩種,由其ordered=
參數控制。
os_data_pd = ["Linux", "macOS", "Windows"]
os_cat_non_ordered = pd.CategoricalDtype(categories=os_data_pd)
os_cat_ordered = pd.CategoricalDtype(categories=os_data_pd, ordered=True)
df_pd = pd.DataFrame({"os": os_data_pd}).assign(
os_cat_non_ordered=lambda df_: df_.os.astype(
{"os": os_cat_non_ordered}
),
os_cat_ordered=lambda df_: df_.os.astype({"os": os_cat_ordered}),
)
os os_cat_non_ordered os_cat_ordered
0 Linux Linux Linux
1 macOS macOS macOS
2 Windows Windows Windows
如果是將無序的pd.CategoricalDtype
與其內含的種類進行比較時,會報錯如下:
❌
# TypeError: Unordered Categoricals can only compare equality or not
df_pd.query("os_cat_non_ordered > 'macOS'")
而如果是有序的pd.CategoricalDtype
,則可以順利與其內含的種類進行比較,例如:
df_pd.query("os_cat_ordered > 'macOS'")
os os_cat_non_ordered os_cat_ordered
2 Windows Windows Windows
註1:當pl.Categorical
與字串或pl.String
進行比較時,若元素不在預先定義的pl.Categorical
內時,並不會報錯,仍然可以進行比較。舉例來說,下面這個例子,我們比較「"os"」列是否大於「"A"」字串:
# ord("A)=65, ord("L")=76, ord("W")=87, ord("m")=109,
# "A" < "Linux" < "Windows" < "macOS"
df0 = pl.DataFrame({"os": os_data}, schema={"os": pl.Categorical()})
print(df0.with_columns(pl.col("os").gt("A").alias("> A")))
shape: (3, 2)
┌─────────┬──────┐
│ os ┆ > A │
│ --- ┆ --- │
│ cat ┆ bool │
╞═════════╪══════╡
│ macOS ┆ true │
│ Linux ┆ true │
│ Windows ┆ true │
└─────────┴──────┘
「"A"」字串雖然不在預先給定的pl.Categorical
中,但仍可與「"os"」列進行比較。由於所有元素的ord
值皆大於「"A"」字串的ord
值(65),所以比較結果皆為True
。